home *** CD-ROM | disk | FTP | other *** search
/ Freelog 115 / FreelogNo115-MaiJuin2013.iso / Internet / AvantBrowser / asetup.exe / _data / webkit / resources.pak / Unnamed File 000004.txt < prev    next >
Text File  |  2013-04-03  |  51KB  |  1,713 lines

  1. // Copyright (c) 2012 The Chromium Authors. All rights reserved.
  2. // Use of this source code is governed by a BSD-style license that can be
  3. // found in the LICENSE file.
  4.  
  5. /** @const */ var BookmarkList = bmm.BookmarkList;
  6. /** @const */ var BookmarkTree = bmm.BookmarkTree;
  7. /** @const */ var Command = cr.ui.Command;
  8. /** @const */ var CommandBinding = cr.ui.CommandBinding;
  9. /** @const */ var LinkKind = cr.LinkKind;
  10. /** @const */ var ListItem = cr.ui.ListItem;
  11. /** @const */ var Menu = cr.ui.Menu;
  12. /** @const */ var MenuButton = cr.ui.MenuButton;
  13. /** @const */ var Promise = cr.Promise;
  14. /** @const */ var Splitter = cr.ui.Splitter;
  15. /** @const */ var TreeItem = cr.ui.TreeItem;
  16.  
  17. // Sometimes the extension API is not initialized.
  18. if (!chrome.bookmarks)
  19.   console.error('Bookmarks extension API is not available');
  20.  
  21. // Get the localized strings from the backend.
  22. chrome.bookmarkManagerPrivate.getStrings(function(data) {
  23.   // The strings may contain & which we need to strip.
  24.   for (var key in data) {
  25.     data[key] = data[key].replace(/&/, '');
  26.   }
  27.  
  28.   loadTimeData.data = data;
  29.   i18nTemplate.process(document, loadTimeData);
  30.  
  31.   recentTreeItem.label = loadTimeData.getString('recent');
  32.   searchTreeItem.label = loadTimeData.getString('search');
  33.   if (!isRTL())
  34.     searchTreeItem.icon = 'images/bookmark_manager_search.png';
  35.   else
  36.     searchTreeItem.icon = 'images/bookmark_manager_search_rtl.png';
  37. });
  38.  
  39. /**
  40.  * The id of the bookmark root.
  41.  * @type {number}
  42.  * @const
  43.  */
  44. var ROOT_ID = '0';
  45.  
  46. /**
  47.  * Delay for expanding folder when pointer hovers on folder in tree view in
  48.  * milliseconds.
  49.  * @type {number}
  50.  * @const
  51.  */
  52. // TODO(yosin): EXPAND_FOLDER_DELAY should follow system settings. 400ms is
  53. // taken from Windows default settings.
  54. var EXPAND_FOLDER_DELAY = 400;
  55.  
  56. var splitter = document.querySelector('.main > .splitter');
  57. Splitter.decorate(splitter);
  58.  
  59. /**
  60.  * An array containing the BookmarkTreeNodes that were deleted in the last
  61.  * deletion action. This is used for implementing undo.
  62.  * @type {Array.<BookmarkTreeNode>}
  63.  */
  64. var lastDeletedNodes;
  65.  
  66. /**
  67.  *
  68.  * Holds the last DOMTimeStamp when mouse pointer hovers on folder in tree
  69.  * view. Zero means pointer doesn't hover on folder.
  70.  * @type {number}
  71.  */
  72. var lastHoverOnFolderTimeStamp = 0;
  73.  
  74. /**
  75.  * Holds a function that will undo that last action, if global undo is enabled.
  76.  * @type {Function}
  77.  */
  78. var performGlobalUndo;
  79.  
  80. // The splitter persists the size of the left component in the local store.
  81. if ('treeWidth' in localStorage)
  82.   splitter.previousElementSibling.style.width = localStorage['treeWidth'];
  83. splitter.addEventListener('resize', function(e) {
  84.   localStorage['treeWidth'] = splitter.previousElementSibling.style.width;
  85. });
  86.  
  87. BookmarkList.decorate(list);
  88.  
  89. var searchTreeItem = new TreeItem({
  90.   bookmarkId: 'q='
  91. });
  92. bmm.treeLookup[searchTreeItem.bookmarkId] = searchTreeItem;
  93.  
  94. var recentTreeItem = new TreeItem({
  95.   icon: 'images/bookmark_manager_recent.png',
  96.   bookmarkId: 'recent'
  97. });
  98. bmm.treeLookup[recentTreeItem.bookmarkId] = recentTreeItem;
  99.  
  100. BookmarkTree.decorate(tree);
  101.  
  102. tree.addEventListener('change', function() {
  103.   navigateTo(tree.selectedItem.bookmarkId, updateHash);
  104. });
  105.  
  106. /**
  107.  * Adds an event listener to a node that will remove itself after firing once.
  108.  * @param {!Element} node The DOM node to add the listener to.
  109.  * @param {string} name The name of the event listener to add to.
  110.  * @param {function(Event)} handler Function called when the event fires.
  111.  */
  112. function addOneShotEventListener(node, name, handler) {
  113.   var f = function(e) {
  114.     handler(e);
  115.     node.removeEventListener(name, f);
  116.   };
  117.   node.addEventListener(name, f);
  118. }
  119.  
  120. /**
  121.  * Updates the location hash to reflect the current state of the application.
  122.  */
  123. function updateHash() {
  124.   window.location.hash = tree.selectedItem.bookmarkId;
  125. }
  126.  
  127. /**
  128.  * Navigates to a bookmark ID.
  129.  * @param {string} id The ID to navigate to.
  130.  * @param {function()} callback Function called when list view loaded or
  131.  *     displayed specified folder.
  132.  */
  133. function navigateTo(id, callback) {
  134.   if (list.parentId == id) {
  135.     callback();
  136.     return;
  137.   }
  138.  
  139.   addOneShotEventListener(list, 'load', callback);
  140.   updateParentId(id);
  141. }
  142.  
  143. /**
  144.  * Updates the parent ID of the bookmark list and selects the correct tree item.
  145.  * @param {string} id The id.
  146.  */
  147. function updateParentId(id) {
  148.   // Setting list.parentId fires 'load' event.
  149.   list.parentId = id;
  150.  
  151.   // When tree.selectedItem changed, tree view calls navigatTo() then it
  152.   // calls updateHash() when list view displayed specified folder.
  153.   tree.selectedItem = bmm.treeLookup[id] || tree.selectedItem;
  154. }
  155.  
  156. // Process the location hash. This is called by onhashchange and when the page
  157. // is first loaded.
  158. function processHash() {
  159.   var id = window.location.hash.slice(1);
  160.   if (!id) {
  161.     // If we do not have a hash, select first item in the tree.
  162.     id = tree.items[0].bookmarkId;
  163.   }
  164.  
  165.   var valid = false;
  166.   if (/^e=/.test(id)) {
  167.     id = id.slice(2);
  168.  
  169.     // If hash contains e=, edit the item specified.
  170.     chrome.bookmarks.get(id, function(bookmarkNodes) {
  171.       // Verify the node to edit is a valid node.
  172.       if (!bookmarkNodes || bookmarkNodes.length != 1)
  173.         return;
  174.       var bookmarkNode = bookmarkNodes[0];
  175.  
  176.       // After the list reloads, edit the desired bookmark.
  177.       var editBookmark = function(e) {
  178.         var index = list.dataModel.findIndexById(bookmarkNode.id);
  179.         if (index != -1) {
  180.           var sm = list.selectionModel;
  181.           sm.anchorIndex = sm.leadIndex = sm.selectedIndex = index;
  182.           scrollIntoViewAndMakeEditable(index);
  183.         }
  184.       };
  185.  
  186.       navigateTo(bookmarkNode.parentId, editBookmark);
  187.     });
  188.  
  189.     // We handle the two cases of navigating to the bookmark to be edited
  190.     // above. Don't run the standard navigation code below.
  191.     return;
  192.   } else if (/^q=/.test(id)) {
  193.     // In case we got a search hash, update the text input and the
  194.     // bmm.treeLookup to use the new id.
  195.     setSearch(id.slice(2));
  196.     valid = true;
  197.   } else if (id == 'recent') {
  198.     valid = true;
  199.   }
  200.  
  201.   // Navigate to bookmark 'id' (which may be a query of the form q=query).
  202.   if (valid) {
  203.     updateParentId(id);
  204.   } else {
  205.     // We need to verify that this is a correct ID.
  206.     chrome.bookmarks.get(id, function(items) {
  207.       if (items && items.length == 1)
  208.         updateParentId(id);
  209.     });
  210.   }
  211. }
  212.  
  213. // We listen to hashchange so that we can update the currently shown folder when
  214. // the user goes back and forward in the history.
  215. window.addEventListener('hashchange', processHash);
  216.  
  217. // Activate is handled by the open-in-same-window-command.
  218. list.addEventListener('dblclick', function(e) {
  219.   if (e.button == 0)
  220.     $('open-in-same-window-command').execute();
  221. });
  222.  
  223. // The list dispatches an event when the user clicks on the URL or the Show in
  224. // folder part.
  225. list.addEventListener('urlClicked', function(e) {
  226.   getLinkController().openUrlFromEvent(e.url, e.originalEvent);
  227.   chrome.bookmarkManagerPrivate.recordLaunch();
  228. });
  229.  
  230. $('term').onsearch = function(e) {
  231.   setSearch(this.value);
  232. };
  233.  
  234. /**
  235.  * Navigates to the search results for the search text.
  236.  * @param {string} searchText The text to search for.
  237.  */
  238. function setSearch(searchText) {
  239.   if (searchText) {
  240.     // Only update search item if we have a search term. We never want the
  241.     // search item to be for an empty search.
  242.     delete bmm.treeLookup[searchTreeItem.bookmarkId];
  243.     var id = searchTreeItem.bookmarkId = 'q=' + searchText;
  244.     bmm.treeLookup[searchTreeItem.bookmarkId] = searchTreeItem;
  245.   }
  246.  
  247.   var input = $('term');
  248.   // Do not update the input if the user is actively using the text input.
  249.   if (document.activeElement != input)
  250.     input.value = searchText;
  251.  
  252.   if (searchText) {
  253.     tree.add(searchTreeItem);
  254.     tree.selectedItem = searchTreeItem;
  255.   } else {
  256.     // Go "home".
  257.     tree.selectedItem = tree.items[0];
  258.     id = tree.selectedItem.bookmarkId;
  259.   }
  260.  
  261.   // Navigate now and update hash immediately.
  262.   navigateTo(id, updateHash);
  263. }
  264.  
  265. // Handle the logo button UI.
  266. // When the user clicks the button we should navigate "home" and focus the list.
  267. document.querySelector('button.logo').onclick = function(e) {
  268.   setSearch('');
  269.   $('list').focus();
  270. };
  271.  
  272. /**
  273.  * This returns the user visible path to the folder where the bookmark is
  274.  * located.
  275.  * @param {number} parentId The ID of the parent folder.
  276.  * @return {string} The path to the the bookmark,
  277.  */
  278. function getFolder(parentId) {
  279.   var parentNode = tree.getBookmarkNodeById(parentId);
  280.   if (parentNode) {
  281.     var s = parentNode.title;
  282.     if (parentNode.parentId != ROOT_ID) {
  283.       return getFolder(parentNode.parentId) + '/' + s;
  284.     }
  285.     return s;
  286.   }
  287. }
  288.  
  289. tree.addEventListener('load', function(e) {
  290.   // Add hard coded tree items.
  291.   tree.add(recentTreeItem);
  292.   processHash();
  293. });
  294.  
  295. tree.reload();
  296. bmm.addBookmarkModelListeners();
  297.  
  298. var dnd = {
  299.   dragData: null,
  300.  
  301.   getBookmarkElement: function(el) {
  302.     while (el && !el.bookmarkNode) {
  303.       el = el.parentNode;
  304.     }
  305.     return el;
  306.   },
  307.  
  308.   // If we are over the list and the list is showing recent or search result,
  309.   // we cannot drop.
  310.   isOverRecentOrSearch: function(overElement) {
  311.     return (list.isRecent() || list.isSearch()) && list.contains(overElement);
  312.   },
  313.  
  314.   checkEvery_: function(f, overBookmarkNode, overElement) {
  315.     return this.dragData.elements.every(function(element) {
  316.       return f.call(this, element, overBookmarkNode, overElement);
  317.     }, this);
  318.   },
  319.  
  320.   /**
  321.    * @return {boolean} Whether we are currently dragging any folders.
  322.    */
  323.   isDraggingFolders: function() {
  324.     return !!this.dragData && this.dragData.elements.some(function(node) {
  325.       return !node.url;
  326.     });
  327.   },
  328.  
  329.   /**
  330.    * This is a first pass whether we can drop the dragged items.
  331.    *
  332.    * @param {!BookmarkTreeNode} overBookmarkNode The bookmark that we are
  333.    *     currently dragging over.
  334.    * @param {!HTMLElement} overElement The element that we are currently
  335.    *     dragging over.
  336.    * @return {boolean} If this returns false then we know we should not drop
  337.    *     the items. If it returns true we still have to call canDropOn,
  338.    *     canDropAbove and canDropBelow.
  339.    */
  340.   canDrop: function(overBookmarkNode, overElement) {
  341.     var dragData = this.dragData;
  342.     if (!dragData)
  343.       return false;
  344.  
  345.     if (this.isOverRecentOrSearch(overElement))
  346.       return false;
  347.  
  348.     if (!dragData.sameProfile)
  349.       return true;
  350.  
  351.     return this.checkEvery_(this.canDrop_, overBookmarkNode, overElement);
  352.   },
  353.  
  354.   /**
  355.    * Helper for canDrop that only checks one bookmark node.
  356.    * @private
  357.    * @return {boolean} False if dragNode is overBookmarkNode or dragNode is
  358.    *     a folder and overBookmarkNode is descendant of dragNode, otherwise
  359.    *     true.
  360.    */
  361.   canDrop_: function(dragNode, overBookmarkNode, overElement) {
  362.     var dragId = dragNode.id;
  363.  
  364.     if (overBookmarkNode.id == dragId)
  365.       return false;
  366.  
  367.     // If we are dragging a folder, we cannot drop it on any of its descendants.
  368.     var dragBookmarkItem = bmm.treeLookup[dragId];
  369.     var dragBookmarkNode = dragBookmarkItem && dragBookmarkItem.bookmarkNode;
  370.     if (dragBookmarkNode && bmm.contains(dragBookmarkNode, overBookmarkNode)) {
  371.       return false;
  372.     }
  373.  
  374.     return true;
  375.   },
  376.  
  377.   /**
  378.    * Whether we can drop the dragged items above the drop target.
  379.    *
  380.    * @param {!BookmarkTreeNode} overBookmarkNode The bookmark that we are
  381.    *     currently dragging over.
  382.    * @param {!HTMLElement} overElement The element that we are currently
  383.    *     dragging over.
  384.    * @return {boolean} Whether we can drop the dragged items above the drop
  385.    *     target.
  386.    */
  387.   canDropAbove: function(overBookmarkNode, overElement) {
  388.     if (overElement instanceof BookmarkList)
  389.       return false;
  390.  
  391.     // We cannot drop between Bookmarks bar and Other bookmarks.
  392.     if (overBookmarkNode.parentId == ROOT_ID)
  393.       return false;
  394.  
  395.     var isOverTreeItem = overElement instanceof TreeItem;
  396.  
  397.     // We can only drop between items in the tree if we have any folders.
  398.     if (isOverTreeItem && !this.isDraggingFolders())
  399.       return false;
  400.  
  401.     if (!this.dragData.sameProfile)
  402.       return this.isDraggingFolders() || !isOverTreeItem;
  403.  
  404.     return this.checkEvery_(this.canDropAbove_, overBookmarkNode, overElement);
  405.   },
  406.  
  407.   /**
  408.    * Helper for canDropAbove that only checks one bookmark node.
  409.    * @private
  410.    * @return {boolean} True if we can drop dragNode above overBookmarkNode,
  411.    *     otherwise false.
  412.    */
  413.   canDropAbove_: function(dragNode, overBookmarkNode, overElement) {
  414.     var dragId = dragNode.id;
  415.  
  416.     // We cannot drop above if the item below is already in the drag source.
  417.     var previousElement = overElement.previousElementSibling;
  418.     if (previousElement &&
  419.         previousElement.bookmarkId == dragId)
  420.       return false;
  421.  
  422.     return true;
  423.   },
  424.  
  425.   /**
  426.    * Whether we can drop the dragged items below the drop target.
  427.    *
  428.    * @param {!BookmarkTreeNode} overBookmarkNode The bookmark that we are
  429.    *     currently dragging over.
  430.    * @param {!HTMLElement} overElement The element that we are currently
  431.    *     dragging over.
  432.    * @return {boolean} Whether we can drop the dragged items below the drop
  433.    *     target.
  434.    */
  435.   canDropBelow: function(overBookmarkNode, overElement) {
  436.     if (overElement instanceof BookmarkList)
  437.       return false;
  438.  
  439.     // We cannot drop between Bookmarks bar and Other bookmarks.
  440.     if (overBookmarkNode.parentId == ROOT_ID)
  441.       return false;
  442.  
  443.     // We can only drop between items in the tree if we have any folders.
  444.     if (!this.isDraggingFolders() && overElement instanceof TreeItem)
  445.       return false;
  446.  
  447.     var isOverTreeItem = overElement instanceof TreeItem;
  448.  
  449.     // Don't allow dropping below an expanded tree item since it is confusing
  450.     // to the user anyway.
  451.     if (isOverTreeItem && overElement.expanded)
  452.       return false;
  453.  
  454.     if (!this.dragData.sameProfile)
  455.       return this.isDraggingFolders() || !isOverTreeItem;
  456.  
  457.     return this.checkEvery_(this.canDropBelow_, overBookmarkNode, overElement);
  458.   },
  459.  
  460.   /**
  461.    * Helper for canDropBelow that only checks one bookmark node.
  462.    * @private
  463.    * @return {boolean} True if we can drop dragNode below overBookmarkNode,
  464.    *     otherwise false.
  465.    */
  466.   canDropBelow_: function(dragNode, overBookmarkNode, overElement) {
  467.     var dragId = dragNode.id;
  468.  
  469.     // We cannot drop below if the item below is already in the drag source.
  470.     var nextElement = overElement.nextElementSibling;
  471.     if (nextElement &&
  472.         nextElement.bookmarkId == dragId)
  473.       return false;
  474.  
  475.     return true;
  476.   },
  477.  
  478.   /**
  479.    * Whether we can drop the dragged items on the drop target.
  480.    *
  481.    * @param {!BookmarkTreeNode} overBookmarkNode The bookmark that we are
  482.    *     currently dragging over.
  483.    * @param {!HTMLElement} overElement The element that we are currently
  484.    *     dragging over.
  485.    * @return {boolean} Whether we can drop the dragged items on the drop
  486.    *     target.
  487.    */
  488.   canDropOn: function(overBookmarkNode, overElement) {
  489.     // We can only drop on a folder.
  490.     if (!bmm.isFolder(overBookmarkNode))
  491.       return false;
  492.  
  493.     if (!this.dragData.sameProfile)
  494.       return true;
  495.  
  496.     return this.checkEvery_(this.canDropOn_, overBookmarkNode, overElement);
  497.   },
  498.  
  499.   /**
  500.    * Helper for canDropOn that only checks one bookmark node.
  501.    * @private
  502.    * @return {boolean} True if dragNode can drop on overBookmarkNode.
  503.    */
  504.   canDropOn_: function(dragNode, overBookmarkNode, overElement) {
  505.     var dragId = dragNode.id;
  506.  
  507.     if (overElement instanceof BookmarkList) {
  508.       // We are trying to drop an item after the last item in the list. This
  509.       // is allowed if the item is different from the last item in the list.
  510.       var listItems = list.items;
  511.       var len = listItems.length;
  512.       if (len == 0 ||
  513.           listItems[len - 1].bookmarkId != dragId) {
  514.         return true;
  515.       }
  516.     }
  517.  
  518.     // Cannot drop on current parent.
  519.     if (overBookmarkNode.id == dragNode.parentId)
  520.       return false;
  521.  
  522.     return true;
  523.   },
  524.  
  525.   /**
  526.    * Callback for the dragstart event.
  527.    * @param {Event} e The dragstart event.
  528.    */
  529.   handleDragStart: function(e) {
  530.     // Determine the selected bookmarks.
  531.     var target = e.target;
  532.     var draggedNodes = [];
  533.     if (target instanceof ListItem) {
  534.       // Use selected items.
  535.       draggedNodes = target.parentNode.selectedItems;
  536.     } else if (target instanceof TreeItem) {
  537.       draggedNodes.push(target.bookmarkNode);
  538.     }
  539.  
  540.     // We manage starting the drag by using the extension API.
  541.     e.preventDefault();
  542.  
  543.     if (draggedNodes.length) {
  544.       // If we are dragging a single link, we can do the *Link* effect.
  545.       // Otherwise, we only allow copy and move.
  546.       var effectAllowed;
  547.       if (draggedNodes.length == 1 &&
  548.           !bmm.isFolder(draggedNodes[0])) {
  549.         effectAllowed = 'copyMoveLink';
  550.       } else {
  551.         effectAllowed = 'copyMove';
  552.       }
  553.       e.dataTransfer.effectAllowed = effectAllowed;
  554.  
  555.       var ids = draggedNodes.map(function(node) {
  556.         return node.id;
  557.       });
  558.  
  559.       chrome.bookmarkManagerPrivate.startDrag(ids);
  560.     }
  561.   },
  562.  
  563.   handleDragEnter: function(e) {
  564.     e.preventDefault();
  565.   },
  566.  
  567.   /**
  568.    * Calback for the dragover event.
  569.    * @param {Event} e The dragover event.
  570.    */
  571.   handleDragOver: function(e) {
  572.     // TODO(arv): This function is way too long. Please refactor it.
  573.  
  574.     // Allow DND on text inputs.
  575.     if (e.target.tagName != 'INPUT') {
  576.       // The default operation is to allow dropping links etc to do navigation.
  577.       // We never want to do that for the bookmark manager.
  578.       e.preventDefault();
  579.  
  580.       // Set to none. This will get set to something if we can do the drop.
  581.       e.dataTransfer.dropEffect = 'none';
  582.     }
  583.  
  584.     if (!this.dragData)
  585.       return;
  586.  
  587.     var overElement = this.getBookmarkElement(e.target);
  588.     if (!overElement && e.target == list)
  589.       overElement = list;
  590.  
  591.     if (!overElement)
  592.       return;
  593.  
  594.     var overBookmarkNode = overElement.bookmarkNode;
  595.  
  596.     // Expands a folder in tree view when pointer hovers on it longer than
  597.     // EXPAND_FOLDER_DELAY.
  598.     var hoverOnFolderTimeStamp = lastHoverOnFolderTimeStamp;
  599.     lastHoverOnFolderTimeStamp = 0;
  600.     if (hoverOnFolderTimeStamp) {
  601.       if (e.timeStamp - hoverOnFolderTimeStamp >= EXPAND_FOLDER_DELAY)
  602.         overElement.expanded = true;
  603.       else
  604.         lastHoverOnFolderTimeStamp = hoverOnFolderTimeStamp;
  605.     } else if (overElement instanceof TreeItem &&
  606.                bmm.isFolder(overBookmarkNode) &&
  607.                overElement.hasChildren &&
  608.                !overElement.expanded) {
  609.       lastHoverOnFolderTimeStamp = e.timeStamp;
  610.     }
  611.  
  612.     if (!this.canDrop(overBookmarkNode, overElement))
  613.       return;
  614.  
  615.     var bookmarkNode = overElement.bookmarkNode;
  616.  
  617.     var canDropAbove = this.canDropAbove(overBookmarkNode, overElement);
  618.     var canDropOn = this.canDropOn(overBookmarkNode, overElement);
  619.     var canDropBelow = this.canDropBelow(overBookmarkNode, overElement);
  620.  
  621.     if (!canDropAbove && !canDropOn && !canDropBelow)
  622.       return;
  623.  
  624.     // Now we know that we can drop. Determine if we will drop above, on or
  625.     // below based on mouse position etc.
  626.  
  627.     var dropPos;
  628.  
  629.     e.dataTransfer.dropEffect = this.dragData.sameProfile ? 'move' : 'copy';
  630.  
  631.     var rect;
  632.     if (overElement instanceof TreeItem) {
  633.       // We only want the rect of the row representing the item and not
  634.       // its children.
  635.       rect = overElement.rowElement.getBoundingClientRect();
  636.     } else {
  637.       rect = overElement.getBoundingClientRect();
  638.     }
  639.  
  640.     var dy = e.clientY - rect.top;
  641.     var yRatio = dy / rect.height;
  642.  
  643.     // above
  644.     if (canDropAbove &&
  645.         (yRatio <= .25 || yRatio <= .5 && !(canDropBelow && canDropOn))) {
  646.       dropPos = 'above';
  647.  
  648.     // below
  649.     } else if (canDropBelow &&
  650.                (yRatio > .75 || yRatio > .5 && !(canDropAbove && canDropOn))) {
  651.       dropPos = 'below';
  652.  
  653.     // on
  654.     } else if (canDropOn) {
  655.       dropPos = 'on';
  656.  
  657.     // none
  658.     } else {
  659.       // No drop can happen. Exit now.
  660.       e.dataTransfer.dropEffect = 'none';
  661.       return;
  662.     }
  663.  
  664.     function cloneClientRect(rect) {
  665.       var newRect = {};
  666.       for (var key in rect) {
  667.         newRect[key] = rect[key];
  668.       }
  669.       return newRect;
  670.     }
  671.  
  672.     // If we are dropping above or below a tree item, adjust the width so
  673.     // that it is clearer where the item will be dropped.
  674.     if ((dropPos == 'above' || dropPos == 'below') &&
  675.         overElement instanceof TreeItem) {
  676.       // ClientRect is read only so clone into a read-write object.
  677.       rect = cloneClientRect(rect);
  678.       var rtl = getComputedStyle(overElement).direction == 'rtl';
  679.       var labelElement = overElement.labelElement;
  680.       var labelRect = labelElement.getBoundingClientRect();
  681.       if (rtl) {
  682.         rect.width = labelRect.left + labelRect.width - rect.left;
  683.       } else {
  684.         rect.left = labelRect.left;
  685.         rect.width -= rect.left;
  686.       }
  687.     }
  688.  
  689.     var overlayType = dropPos;
  690.  
  691.     // If we are dropping on a list we want to show an overlay drop line after
  692.     // the last element.
  693.     if (overElement instanceof BookmarkList) {
  694.       overlayType = 'below';
  695.  
  696.       // Get the rect of the last list item.
  697.       var length = overElement.dataModel.length;
  698.       if (length) {
  699.         dropPos = 'below';
  700.         overElement = overElement.getListItemByIndex(length - 1);
  701.         rect = overElement.getBoundingClientRect();
  702.       } else {
  703.         // If there are no items, collapse the height of the rect.
  704.         rect = cloneClientRect(rect);
  705.         rect.height = 0;
  706.         // We do not use bottom so we don't care to adjust it.
  707.       }
  708.     }
  709.  
  710.     this.showDropOverlay_(rect, overlayType);
  711.  
  712.     this.dropDestination = {
  713.       dropPos: dropPos,
  714.       relatedNode: overElement.bookmarkNode
  715.     };
  716.   },
  717.  
  718.   /**
  719.    * Shows and positions the drop marker overlay.
  720.    * @param {ClientRect} targetRect The drop target rect.
  721.    * @param {string} overlayType The position relative to the target rect.
  722.    * @private
  723.    */
  724.   showDropOverlay_: function(targetRect, overlayType) {
  725.     window.clearTimeout(this.hideDropOverlayTimer_);
  726.     var overlay = $('drop-overlay');
  727.     if (overlayType == 'on') {
  728.       overlay.className = '';
  729.       overlay.style.top = targetRect.top + 'px';
  730.       overlay.style.height = targetRect.height + 'px';
  731.     } else {
  732.       overlay.className = 'line';
  733.       overlay.style.height = '';
  734.     }
  735.     overlay.style.width = targetRect.width + 'px';
  736.     overlay.style.left = targetRect.left + 'px';
  737.     overlay.style.display = 'block';
  738.  
  739.     if (overlayType != 'on') {
  740.       var overlayRect = overlay.getBoundingClientRect();
  741.       if (overlayType == 'above') {
  742.         overlay.style.top = targetRect.top - overlayRect.height / 2 + 'px';
  743.       } else {
  744.         overlay.style.top = targetRect.top + targetRect.height -
  745.             overlayRect.height / 2 + 'px';
  746.       }
  747.     }
  748.   },
  749.  
  750.   /**
  751.    * Hides the drop overlay element.
  752.    * @private
  753.    */
  754.   hideDropOverlay_: function() {
  755.     // Hide the overlay in a timeout to reduce flickering as we move between
  756.     // valid drop targets.
  757.     window.clearTimeout(this.hideDropOverlayTimer_);
  758.     this.hideDropOverlayTimer_ = window.setTimeout(function() {
  759.       $('drop-overlay').style.display = '';
  760.     }, 100);
  761.   },
  762.  
  763.   handleDragLeave: function(e) {
  764.     this.hideDropOverlay_();
  765.   },
  766.  
  767.   handleDrop: function(e) {
  768.     if (this.dropDestination && this.dragData) {
  769.       var dropPos = this.dropDestination.dropPos;
  770.       var relatedNode = this.dropDestination.relatedNode;
  771.       var parentId = dropPos == 'on' ? relatedNode.id : relatedNode.parentId;
  772.  
  773.       var selectTarget;
  774.       var selectedTreeId;
  775.       var index;
  776.       var relatedIndex;
  777.       // Try to find the index in the dataModel so we don't have to always keep
  778.       // the index for the list items up to date.
  779.       var overElement = this.getBookmarkElement(e.target);
  780.       if (overElement instanceof ListItem) {
  781.         relatedIndex = overElement.parentNode.dataModel.indexOf(relatedNode);
  782.         selectTarget = list;
  783.       } else if (overElement instanceof BookmarkList) {
  784.         relatedIndex = overElement.dataModel.length - 1;
  785.         selectTarget = list;
  786.       } else {
  787.         // Tree
  788.         relatedIndex = relatedNode.index;
  789.         selectTarget = tree;
  790.         selectedTreeId =
  791.             tree.selectedItem ? tree.selectedItem.bookmarkId : null;
  792.       }
  793.  
  794.       if (dropPos == 'above')
  795.         index = relatedIndex;
  796.       else if (dropPos == 'below')
  797.         index = relatedIndex + 1;
  798.  
  799.       selectItemsAfterUserAction(selectTarget, selectedTreeId);
  800.  
  801.       if (index != undefined && index != -1)
  802.         chrome.bookmarkManagerPrivate.drop(parentId, index);
  803.       else
  804.         chrome.bookmarkManagerPrivate.drop(parentId);
  805.  
  806.       e.preventDefault();
  807.  
  808.       // TODO(arv): Select the newly dropped items.
  809.     }
  810.     this.dropDestination = null;
  811.     this.hideDropOverlay_();
  812.   },
  813.  
  814.   clearDragData: function() {
  815.     this.dragData = null;
  816.   },
  817.  
  818.   handleChromeDragEnter: function(dragData) {
  819.     this.dragData = dragData;
  820.   },
  821.  
  822.   init: function() {
  823.     var boundClearData = this.clearDragData.bind(this);
  824.     function deferredClearData() {
  825.       setTimeout(boundClearData);
  826.     }
  827.  
  828.     document.addEventListener('dragstart', this.handleDragStart.bind(this));
  829.     document.addEventListener('dragenter', this.handleDragEnter.bind(this));
  830.     document.addEventListener('dragover', this.handleDragOver.bind(this));
  831.     document.addEventListener('dragleave', this.handleDragLeave.bind(this));
  832.     document.addEventListener('drop', this.handleDrop.bind(this));
  833.     document.addEventListener('dragend', deferredClearData);
  834.     document.addEventListener('mouseup', deferredClearData);
  835.  
  836.     chrome.bookmarkManagerPrivate.onDragEnter.addListener(
  837.         this.handleChromeDragEnter.bind(this));
  838.     chrome.bookmarkManagerPrivate.onDragLeave.addListener(
  839.         deferredClearData);
  840.     chrome.bookmarkManagerPrivate.onDrop.addListener(deferredClearData);
  841.   }
  842. };
  843.  
  844. dnd.init();
  845.  
  846. // Commands
  847.  
  848. cr.ui.decorate('menu', Menu);
  849. cr.ui.decorate('button[menu]', MenuButton);
  850. cr.ui.decorate('command', Command);
  851.  
  852. cr.ui.contextMenuHandler.addContextMenuProperty(tree);
  853. list.contextMenu = $('context-menu');
  854. tree.contextMenu = $('context-menu');
  855.  
  856. // Disable almost all commands at startup.
  857. var commands = document.querySelectorAll('command');
  858. for (var i = 0, command; command = commands[i]; i++) {
  859.   if (command.id != 'import-menu-command' &&
  860.       command.id != 'export-menu-command') {
  861.     command.disabled = true;
  862.   }
  863. }
  864.  
  865. var canEdit = true;
  866. chrome.bookmarkManagerPrivate.canEdit(function(result) {
  867.   canEdit = result;
  868. });
  869.  
  870. /**
  871.  * Incognito mode availability can take the following values: ,
  872.  *   - 'enabled' for when both normal and incognito modes are available;
  873.  *   - 'disabled' for when incognito mode is disabled;
  874.  *   - 'forced' for when incognito mode is forced (normal mode is unavailable).
  875.  */
  876. var incognitoModeAvailability = 'enabled';
  877. chrome.systemPrivate.getIncognitoModeAvailability(function(result) {
  878.   // TODO(rustema): propagate policy value to the bookmark manager when
  879.   //                it changes.
  880.   incognitoModeAvailability = result;
  881. });
  882.  
  883. /**
  884.  * New Windows are not allowed in Windows 8 metro mode.
  885.  */
  886. var canOpenNewWindows = true;
  887. chrome.bookmarkManagerPrivate.canOpenNewWindows(function(result) {
  888.     canOpenNewWindows = result;
  889. });
  890.  
  891. /**
  892.  * Helper function that updates the canExecute and labels for the open-like
  893.  * commands.
  894.  * @param {!cr.ui.CanExecuteEvent} e The event fired by the command system.
  895.  * @param {!cr.ui.Command} command The command we are currently processing.
  896.  */
  897. function updateOpenCommands(e, command) {
  898.   var selectedItem = e.target.selectedItem;
  899.   var selectionCount;
  900.   if (e.target == tree) {
  901.     selectionCount = selectedItem ? 1 : 0;
  902.     selectedItem = selectedItem.bookmarkNode;
  903.   } else {
  904.     selectionCount = list.selectedItems.length;
  905.   }
  906.  
  907.   var isFolder = selectionCount == 1 &&
  908.                  selectedItem &&
  909.                  bmm.isFolder(selectedItem);
  910.   var multiple = selectionCount != 1 || isFolder;
  911.  
  912.   function hasBookmarks(node) {
  913.     for (var i = 0; i < node.children.length; i++) {
  914.       if (!bmm.isFolder(node.children[i]))
  915.         return true;
  916.     }
  917.     return false;
  918.   }
  919.  
  920.   var commandDisabled = false;
  921.   switch (command.id) {
  922.     case 'open-in-new-tab-command':
  923.       command.label = loadTimeData.getString(multiple ?
  924.           'open_all' : 'open_in_new_tab');
  925.       break;
  926.  
  927.     case 'open-in-new-window-command':
  928.       command.label = loadTimeData.getString(multiple ?
  929.           'open_all_new_window' : 'open_in_new_window');
  930.       // Disabled when incognito is forced.
  931.       commandDisabled = incognitoModeAvailability == 'forced' ||
  932.           !canOpenNewWindows;
  933.       break;
  934.     case 'open-incognito-window-command':
  935.       command.label = loadTimeData.getString(multiple ?
  936.           'open_all_incognito' : 'open_incognito');
  937.       // Not available withn incognito is disabled.
  938.       commandDisabled = incognitoModeAvailability == 'disabled';
  939.       break;
  940.   }
  941.   e.canExecute = selectionCount > 0 && !!selectedItem && !commandDisabled;
  942.   if (isFolder && e.canExecute) {
  943.     // We need to get all the bookmark items in this tree. If the tree does not
  944.     // contain any non-folders, we need to disable the command.
  945.     var p = bmm.loadSubtree(selectedItem.id);
  946.     p.addListener(function(node) {
  947.       command.disabled = !node || !hasBookmarks(node);
  948.     });
  949.   }
  950. }
  951.  
  952. /**
  953.  * Calls the backend to figure out if we can paste the clipboard into the active
  954.  * folder.
  955.  * @param {Function=} opt_f Function to call after the state has been updated.
  956.  */
  957. function updatePasteCommand(opt_f) {
  958.   function update(canPaste) {
  959.     var command = $('paste-command');
  960.     command.disabled = !canPaste;
  961.     if (opt_f)
  962.       opt_f();
  963.   }
  964.   // We cannot paste into search and recent view.
  965.   if (list.isSearch() || list.isRecent()) {
  966.     update(false);
  967.   } else {
  968.     chrome.bookmarkManagerPrivate.canPaste(list.parentId, update);
  969.   }
  970. }
  971.  
  972. document.addEventListener('canExecute', function(e) {
  973.   var command = e.command;
  974.   switch (command.id) {
  975.     case 'import-menu-command':
  976.       e.canExecute = canEdit;
  977.       break;
  978.     case 'export-menu-command':
  979.       // We can always execute the export-menu command.
  980.       e.canExecute = true;
  981.       break;
  982.     case 'sort-command':
  983.       e.canExecute = !list.isRecent() && !list.isSearch() &&
  984.           list.dataModel.length > 1;
  985.       break;
  986.     case 'undo-command':
  987.       // The global undo command has no visible UI, so always enable it, and
  988.       // just make it a no-op if undo is not possible.
  989.       e.canExecute = true;
  990.       break;
  991.     default:
  992.       canExecuteForList(e);
  993.       break;
  994.   }
  995. });
  996.  
  997. /**
  998.  * Helper function for handling canExecute for the list and the tree.
  999.  * @param {!Event} e Can execute event object.
  1000.  * @param {boolean} isRecentOrSearch Whether the user is trying to do a command
  1001.  *     on recent or search.
  1002.  */
  1003. function canExecuteShared(e, isRecentOrSearch) {
  1004.   var command = e.command;
  1005.   var commandId = command.id;
  1006.   switch (commandId) {
  1007.     case 'paste-command':
  1008.       updatePasteCommand();
  1009.       break;
  1010.  
  1011.     case 'add-new-bookmark-command':
  1012.     case 'new-folder-command':
  1013.       e.canExecute = !isRecentOrSearch && canEdit;
  1014.       break;
  1015.  
  1016.     case 'open-in-new-tab-command':
  1017.     case 'open-in-background-tab-command':
  1018.     case 'open-in-new-window-command':
  1019.     case 'open-incognito-window-command':
  1020.       updateOpenCommands(e, command);
  1021.       break;
  1022.  
  1023.     case 'undo-delete-command':
  1024.       e.canExecute = !!lastDeletedNodes;
  1025.       break;
  1026.   }
  1027. }
  1028.  
  1029. /**
  1030.  * Helper function for handling canExecute for the list and document.
  1031.  * @param {!Event} e Can execute event object.
  1032.  */
  1033. function canExecuteForList(e) {
  1034.   var command = e.command;
  1035.   var commandId = command.id;
  1036.  
  1037.   function hasSelected() {
  1038.     return !!list.selectedItem;
  1039.   }
  1040.  
  1041.   function hasSingleSelected() {
  1042.     return list.selectedItems.length == 1;
  1043.   }
  1044.  
  1045.   function canCopyItem(item) {
  1046.     return item.id != 'new';
  1047.   }
  1048.  
  1049.   function canCopyItems() {
  1050.     var selectedItems = list.selectedItems;
  1051.     return selectedItems && selectedItems.some(canCopyItem);
  1052.   }
  1053.  
  1054.   function isRecentOrSearch() {
  1055.     return list.isRecent() || list.isSearch();
  1056.   }
  1057.  
  1058.   switch (commandId) {
  1059.     case 'rename-folder-command':
  1060.       // Show rename if a single folder is selected.
  1061.       var items = list.selectedItems;
  1062.       if (items.length != 1) {
  1063.         e.canExecute = false;
  1064.         command.hidden = true;
  1065.       } else {
  1066.         var isFolder = bmm.isFolder(items[0]);
  1067.         e.canExecute = isFolder && canEdit;
  1068.         command.hidden = !isFolder;
  1069.       }
  1070.       break;
  1071.  
  1072.     case 'edit-command':
  1073.       // Show the edit command if not a folder.
  1074.       var items = list.selectedItems;
  1075.       if (items.length != 1) {
  1076.         e.canExecute = false;
  1077.         command.hidden = false;
  1078.       } else {
  1079.         var isFolder = bmm.isFolder(items[0]);
  1080.         e.canExecute = !isFolder && canEdit;
  1081.         command.hidden = isFolder;
  1082.       }
  1083.       break;
  1084.  
  1085.     case 'show-in-folder-command':
  1086.       e.canExecute = isRecentOrSearch() && hasSingleSelected();
  1087.       break;
  1088.  
  1089.     case 'delete-command':
  1090.     case 'cut-command':
  1091.       e.canExecute = canCopyItems() && canEdit;
  1092.       break;
  1093.  
  1094.     case 'copy-command':
  1095.       e.canExecute = canCopyItems();
  1096.       break;
  1097.  
  1098.     case 'open-in-same-window-command':
  1099.       e.canExecute = hasSelected();
  1100.       break;
  1101.  
  1102.     default:
  1103.       canExecuteShared(e, isRecentOrSearch());
  1104.   }
  1105. }
  1106.  
  1107. // Update canExecute for the commands when the list is the active element.
  1108. list.addEventListener('canExecute', function(e) {
  1109.   if (e.target != list) return;
  1110.   canExecuteForList(e);
  1111. });
  1112.  
  1113. // Update canExecute for the commands when the tree is the active element.
  1114. tree.addEventListener('canExecute', function(e) {
  1115.   if (e.target != tree) return;
  1116.  
  1117.   var command = e.command;
  1118.   var commandId = command.id;
  1119.  
  1120.   function hasSelected() {
  1121.     return !!e.target.selectedItem;
  1122.   }
  1123.  
  1124.   function isRecentOrSearch() {
  1125.     var item = e.target.selectedItem;
  1126.     return item == recentTreeItem || item == searchTreeItem;
  1127.   }
  1128.  
  1129.   function isTopLevelItem() {
  1130.     return e.target.selectedItem.parentNode == tree;
  1131.   }
  1132.  
  1133.   switch (commandId) {
  1134.     case 'rename-folder-command':
  1135.       command.hidden = false;
  1136.       e.canExecute = hasSelected() && !isTopLevelItem() && canEdit;
  1137.       break;
  1138.  
  1139.     case 'edit-command':
  1140.       command.hidden = true;
  1141.       e.canExecute = false;
  1142.       break;
  1143.  
  1144.     case 'delete-command':
  1145.     case 'cut-command':
  1146.       e.canExecute = hasSelected() && !isTopLevelItem() && canEdit;
  1147.       break;
  1148.  
  1149.     case 'copy-command':
  1150.       e.canExecute = hasSelected() && !isTopLevelItem();
  1151.       break;
  1152.  
  1153.     default:
  1154.       canExecuteShared(e, isRecentOrSearch());
  1155.   }
  1156. });
  1157.  
  1158. /**
  1159.  * Update the canExecute state of the commands when the selection changes.
  1160.  * @param {Event} e The change event object.
  1161.  */
  1162. function updateCommandsBasedOnSelection(e) {
  1163.   if (e.target == document.activeElement) {
  1164.     // Paste only needs to be updated when the tree selection changes.
  1165.     var commandNames = ['copy', 'cut', 'delete', 'rename-folder', 'edit',
  1166.       'add-new-bookmark', 'new-folder', 'open-in-new-tab',
  1167.       'open-in-new-window', 'open-incognito-window', 'open-in-same-window',
  1168.       'show-in-folder'];
  1169.  
  1170.     if (e.target == tree) {
  1171.       commandNames.push('paste', 'sort');
  1172.     }
  1173.  
  1174.     commandNames.forEach(function(baseId) {
  1175.       $(baseId + '-command').canExecuteChange();
  1176.     });
  1177.   }
  1178. }
  1179.  
  1180. list.addEventListener('change', updateCommandsBasedOnSelection);
  1181. tree.addEventListener('change', updateCommandsBasedOnSelection);
  1182.  
  1183. function updateEditingCommands() {
  1184.   var editingCommands = ['cut', 'delete', 'rename-folder', 'edit',
  1185.       'add-new-bookmark', 'new-folder', 'sort', 'paste'];
  1186.  
  1187.   chrome.bookmarkManagerPrivate.canEdit(function(result) {
  1188.     if (result != canEdit) {
  1189.       canEdit = result;
  1190.       editingCommands.forEach(function(baseId) {
  1191.         $(baseId + '-command').canExecuteChange();
  1192.       });
  1193.     }
  1194.   });
  1195. }
  1196.  
  1197. var organizeButton = document.querySelector('.summary > button');
  1198. organizeButton.addEventListener('click', function(e) {
  1199.   updateEditingCommands();
  1200.   $('add-new-bookmark-command').canExecuteChange();
  1201.   $('new-folder-command').canExecuteChange();
  1202.   $('sort-command').canExecuteChange();
  1203. });
  1204. list.addEventListener('contextmenu', updateEditingCommands);
  1205. tree.addEventListener('contextmenu', updateEditingCommands);
  1206.  
  1207. function handleRename(e) {
  1208.   var item = e.target;
  1209.   var bookmarkNode = item.bookmarkNode;
  1210.   chrome.bookmarks.update(bookmarkNode.id, {title: item.label});
  1211.   performGlobalUndo = null;  // This can't be undone, so disable global undo.
  1212. }
  1213.  
  1214. tree.addEventListener('rename', handleRename);
  1215. list.addEventListener('rename', handleRename);
  1216.  
  1217. list.addEventListener('edit', function(e) {
  1218.   var item = e.target;
  1219.   var bookmarkNode = item.bookmarkNode;
  1220.   var context = {
  1221.     title: bookmarkNode.title
  1222.   };
  1223.   if (!bmm.isFolder(bookmarkNode))
  1224.     context.url = bookmarkNode.url;
  1225.  
  1226.   if (bookmarkNode.id == 'new') {
  1227.     selectItemsAfterUserAction(list);
  1228.  
  1229.     // New page
  1230.     context.parentId = bookmarkNode.parentId;
  1231.     chrome.bookmarks.create(context, function(node) {
  1232.       // A new node was created and will get added to the list due to the
  1233.       // handler.
  1234.       var dataModel = list.dataModel;
  1235.       var index = dataModel.indexOf(bookmarkNode);
  1236.       dataModel.splice(index, 1);
  1237.  
  1238.       // Select new item.
  1239.       var newIndex = dataModel.findIndexById(node.id);
  1240.       if (newIndex != -1) {
  1241.         var sm = list.selectionModel;
  1242.         list.scrollIndexIntoView(newIndex);
  1243.         sm.leadIndex = sm.anchorIndex = sm.selectedIndex = newIndex;
  1244.       }
  1245.     });
  1246.   } else {
  1247.     // Edit
  1248.     chrome.bookmarks.update(bookmarkNode.id, context);
  1249.   }
  1250.   performGlobalUndo = null;  // This can't be undone, so disable global undo.
  1251. });
  1252.  
  1253. list.addEventListener('canceledit', function(e) {
  1254.   var item = e.target;
  1255.   var bookmarkNode = item.bookmarkNode;
  1256.   if (bookmarkNode.id == 'new') {
  1257.     var dataModel = list.dataModel;
  1258.     var index = dataModel.findIndexById('new');
  1259.     dataModel.splice(index, 1);
  1260.   }
  1261. });
  1262.  
  1263. /**
  1264.  * Navigates to the folder that the selected item is in and selects it. This is
  1265.  * used for the show-in-folder command.
  1266.  */
  1267. function showInFolder() {
  1268.   var bookmarkNode = list.selectedItem;
  1269.   if (!bookmarkNode)
  1270.     return;
  1271.   var parentId = bookmarkNode.parentId;
  1272.  
  1273.   // After the list is loaded we should select the revealed item.
  1274.   function selectItem() {
  1275.     var index = list.dataModel.findIndexById(bookmarkNode.id);
  1276.     if (index == -1)
  1277.       return;
  1278.     var sm = list.selectionModel;
  1279.     sm.anchorIndex = sm.leadIndex = sm.selectedIndex = index;
  1280.     list.scrollIndexIntoView(index);
  1281.   }
  1282.  
  1283.   var treeItem = bmm.treeLookup[parentId];
  1284.   treeItem.reveal();
  1285.  
  1286.   navigateTo(parentId, selectItem);
  1287. }
  1288.  
  1289. var linkController;
  1290.  
  1291. /**
  1292.  * @return {!cr.LinkController} The link controller used to open links based on
  1293.  *     user clicks and keyboard actions.
  1294.  */
  1295. function getLinkController() {
  1296.   return linkController ||
  1297.       (linkController = new cr.LinkController(loadTimeData));
  1298. }
  1299.  
  1300. /**
  1301.  * Returns the selected bookmark nodes of the active element. Only call this
  1302.  * if the list or the tree is focused.
  1303.  * @return {!Array} Array of bookmark nodes.
  1304.  */
  1305. function getSelectedBookmarkNodes() {
  1306.   return document.activeElement == tree ? [tree.selectedItem.bookmarkNode] :
  1307.                                           list.selectedItems;
  1308. }
  1309.  
  1310. /**
  1311.  * @return {!Array.<string>} An array of the selected bookmark IDs.
  1312.  */
  1313. function getSelectedBookmarkIds() {
  1314.   return getSelectedBookmarkNodes().map(function(node) {
  1315.     return node.id;
  1316.   });
  1317. }
  1318.  
  1319. /**
  1320.  * Opens the selected bookmarks.
  1321.  * @param {LinkKind} kind The kind of link we want to open.
  1322.  */
  1323. function openBookmarks(kind) {
  1324.   // If we have selected any folders, we need to find all items recursively.
  1325.   // We use multiple async calls to getSubtree instead of getting the whole
  1326.   // tree since we would like to minimize the amount of data sent.
  1327.  
  1328.   var urls = [];
  1329.  
  1330.   // Adds the node and all its children.
  1331.   function addNodes(node) {
  1332.     if (node.children) {
  1333.       node.children.forEach(function(child) {
  1334.         if (!bmm.isFolder(child))
  1335.           urls.push(child.url);
  1336.       });
  1337.     } else {
  1338.       urls.push(node.url);
  1339.     }
  1340.   }
  1341.  
  1342.   var nodes = getSelectedBookmarkNodes();
  1343.  
  1344.   // Get a future promise for every selected item.
  1345.   var promises = nodes.map(function(node) {
  1346.     if (bmm.isFolder(node))
  1347.       return bmm.loadSubtree(node.id);
  1348.     // Not a folder so we already have all the data we need.
  1349.     return new Promise(node.url);
  1350.   });
  1351.  
  1352.   var p = Promise.all.apply(null, promises);
  1353.   p.addListener(function(values) {
  1354.     values.forEach(function(v) {
  1355.       if (typeof v == 'string')
  1356.         urls.push(v);
  1357.       else
  1358.         addNodes(v);
  1359.     });
  1360.     getLinkController().openUrls(urls, kind);
  1361.     chrome.bookmarkManagerPrivate.recordLaunch();
  1362.   });
  1363. }
  1364.  
  1365. /**
  1366.  * Opens an item in the list.
  1367.  */
  1368. function openItem() {
  1369.   var bookmarkNodes = getSelectedBookmarkNodes();
  1370.   // If we double clicked or pressed enter on a single folder, navigate to it.
  1371.   if (bookmarkNodes.length == 1 && bmm.isFolder(bookmarkNodes[0])) {
  1372.     navigateTo(bookmarkNodes[0].id, updateHash);
  1373.   } else {
  1374.     openBookmarks(LinkKind.FOREGROUND_TAB);
  1375.   }
  1376. }
  1377.  
  1378. /**
  1379.  * Deletes the selected bookmarks. The bookmarks are saved in memory in case
  1380.  * the user needs to undo the deletion.
  1381.  */
  1382. function deleteBookmarks() {
  1383.   var selectedIds = getSelectedBookmarkIds();
  1384.   lastDeletedNodes = [];
  1385.  
  1386.   function performDelete() {
  1387.     selectedIds.forEach(function(id) {
  1388.       chrome.bookmarks.removeTree(id);
  1389.     });
  1390.     $('undo-delete-command').canExecuteChange();
  1391.     performGlobalUndo = undoDelete;
  1392.   }
  1393.  
  1394.   // First, store information about the bookmarks being deleted.
  1395.   selectedIds.forEach(function(id) {
  1396.     chrome.bookmarks.getSubTree(id, function(results) {
  1397.       lastDeletedNodes.push(results);
  1398.  
  1399.       // When all nodes have been saved, perform the deletion.
  1400.       if (lastDeletedNodes.length === selectedIds.length)
  1401.         performDelete();
  1402.     });
  1403.   });
  1404. }
  1405.  
  1406. /**
  1407.  * Restores a tree of bookmarks under a specified folder.
  1408.  * @param {BookmarkTreeNode} node The node to restore.
  1409.  * @param {=string} parentId The ID of the folder to restore under. If not
  1410.  *     specified, the original parentId of the node will be used.
  1411.  */
  1412. function restoreTree(node, parentId) {
  1413.   var bookmarkInfo = {
  1414.     parentId: parentId || node.parentId,
  1415.     title: node.title,
  1416.     index: node.index,
  1417.     url: node.url
  1418.   };
  1419.  
  1420.   chrome.bookmarks.create(bookmarkInfo, function(result) {
  1421.     if (!result) {
  1422.       console.error('Failed to restore bookmark.');
  1423.       return;
  1424.     }
  1425.  
  1426.     if (node.children) {
  1427.       // Restore the children using the new ID for this node.
  1428.       node.children.forEach(function(child) {
  1429.         restoreTree(child, result.id);
  1430.       });
  1431.     }
  1432.   });
  1433. }
  1434.  
  1435. /**
  1436.  * Restores the last set of bookmarks that was deleted.
  1437.  */
  1438. function undoDelete() {
  1439.   lastDeletedNodes.forEach(function(arr) {
  1440.     arr.forEach(restoreTree);
  1441.   });
  1442.   lastDeletedNodes = null;
  1443.   $('undo-delete-command').canExecuteChange();
  1444.  
  1445.   // Only a single level of undo is supported, so disable global undo now.
  1446.   performGlobalUndo = null;
  1447. }
  1448.  
  1449. /**
  1450.  * Computes folder for "Add Page" and "Add Folder".
  1451.  * @return {string} The id of folder node where we'll create new page/folder.
  1452.  */
  1453. function computeParentFolderForNewItem() {
  1454.   if (document.activeElement == tree)
  1455.     return list.parentId;
  1456.   var selectedItem = list.selectedItem;
  1457.   return selectedItem && bmm.isFolder(selectedItem) ?
  1458.       selectedItem.id : list.parentId;
  1459. }
  1460.  
  1461. /**
  1462.  * Callback for the new folder command. This creates a new folder and starts
  1463.  * a rename of it.
  1464.  */
  1465. function newFolder() {
  1466.   performGlobalUndo = null;  // This can't be undone, so disable global undo.
  1467.  
  1468.   var parentId = computeParentFolderForNewItem();
  1469.  
  1470.   // Callback is called after tree and list data model updated.
  1471.   function createFolder(callback) {
  1472.     chrome.bookmarks.create({
  1473.       title: loadTimeData.getString('new_folder_name'),
  1474.       parentId: parentId
  1475.     }, callback);
  1476.   }
  1477.  
  1478.   if (document.activeElement == tree) {
  1479.     createFolder(function(newNode) {
  1480.       navigateTo(newNode.id, function() {
  1481.         bmm.treeLookup[newNode.id].editing = true;
  1482.       });
  1483.     });
  1484.     return;
  1485.   }
  1486.  
  1487.   function editNewFolderInList() {
  1488.     createFolder(function() {
  1489.       var index = list.dataModel.length - 1;
  1490.       var sm = list.selectionModel;
  1491.       sm.anchorIndex = sm.leadIndex = sm.selectedIndex = index;
  1492.       scrollIntoViewAndMakeEditable(index);
  1493.     });
  1494.   }
  1495.  
  1496.   navigateTo(parentId, editNewFolderInList);
  1497. }
  1498.  
  1499. /**
  1500.  * Scrolls the list item into view and makes it editable.
  1501.  * @param {number} index The index of the item to make editable.
  1502.  */
  1503. function scrollIntoViewAndMakeEditable(index) {
  1504.   list.scrollIndexIntoView(index);
  1505.   // onscroll is now dispatched asynchronously so we have to postpone
  1506.   // the rest.
  1507.   setTimeout(function() {
  1508.     var item = list.getListItemByIndex(index);
  1509.     if (item)
  1510.       item.editing = true;
  1511.   });
  1512. }
  1513.  
  1514. /**
  1515.  * Adds a page to the current folder. This is called by the
  1516.  * add-new-bookmark-command handler.
  1517.  */
  1518. function addPage() {
  1519.   var parentId = computeParentFolderForNewItem();
  1520.  
  1521.   function editNewBookmark() {
  1522.     var fakeNode = {
  1523.       title: '',
  1524.       url: '',
  1525.       parentId: parentId,
  1526.       id: 'new'
  1527.     };
  1528.     var dataModel = list.dataModel;
  1529.     var length = dataModel.length;
  1530.     dataModel.splice(length, 0, fakeNode);
  1531.     var sm = list.selectionModel;
  1532.     sm.anchorIndex = sm.leadIndex = sm.selectedIndex = length;
  1533.     scrollIntoViewAndMakeEditable(length);
  1534.   };
  1535.  
  1536.   navigateTo(parentId, editNewBookmark);
  1537. }
  1538.  
  1539. /**
  1540.  * This function is used to select items after a user action such as paste, drop
  1541.  * add page etc.
  1542.  * @param {BookmarkList|BookmarkTree} target The target of the user action.
  1543.  * @param {=string} opt_selectedTreeId If provided, then select that tree id.
  1544.  */
  1545. function selectItemsAfterUserAction(target, opt_selectedTreeId) {
  1546.   // We get one onCreated event per item so we delay the handling until we get
  1547.   // no more events coming.
  1548.  
  1549.   var ids = [];
  1550.   var timer;
  1551.  
  1552.   function handle(id, bookmarkNode) {
  1553.     clearTimeout(timer);
  1554.     if (opt_selectedTreeId || list.parentId == bookmarkNode.parentId)
  1555.       ids.push(id);
  1556.     timer = setTimeout(handleTimeout, 50);
  1557.   }
  1558.  
  1559.   function handleTimeout() {
  1560.     chrome.bookmarks.onCreated.removeListener(handle);
  1561.     chrome.bookmarks.onMoved.removeListener(handle);
  1562.  
  1563.     if (opt_selectedTreeId && ids.indexOf(opt_selectedTreeId) != -1) {
  1564.       var index = ids.indexOf(opt_selectedTreeId);
  1565.       if (index != -1 && opt_selectedTreeId in bmm.treeLookup) {
  1566.         tree.selectedItem = bmm.treeLookup[opt_selectedTreeId];
  1567.       }
  1568.     } else if (target == list) {
  1569.       var dataModel = list.dataModel;
  1570.       var firstIndex = dataModel.findIndexById(ids[0]);
  1571.       var lastIndex = dataModel.findIndexById(ids[ids.length - 1]);
  1572.       if (firstIndex != -1 && lastIndex != -1) {
  1573.         var selectionModel = list.selectionModel;
  1574.         selectionModel.selectedIndex = -1;
  1575.         selectionModel.selectRange(firstIndex, lastIndex);
  1576.         selectionModel.anchorIndex = selectionModel.leadIndex = lastIndex;
  1577.         list.focus();
  1578.       }
  1579.     }
  1580.  
  1581.     list.endBatchUpdates();
  1582.   }
  1583.  
  1584.   list.startBatchUpdates();
  1585.  
  1586.   chrome.bookmarks.onCreated.addListener(handle);
  1587.   chrome.bookmarks.onMoved.addListener(handle);
  1588.   timer = setTimeout(handleTimeout, 300);
  1589. }
  1590.  
  1591. /**
  1592.  * Handler for the command event. This is used for context menu of list/tree
  1593.  * and organized menu.
  1594.  * @param {!Event} e The event object.
  1595.  */
  1596. function handleCommand(e) {
  1597.   var command = e.command;
  1598.   var commandId = command.id;
  1599.   switch (commandId) {
  1600.     case 'import-menu-command':
  1601.       chrome.bookmarks.import();
  1602.       break;
  1603.     case 'export-menu-command':
  1604.       chrome.bookmarks.export();
  1605.       break;
  1606.     case 'undo-command':
  1607.       if (performGlobalUndo)
  1608.         performGlobalUndo();
  1609.       break;
  1610.     case 'show-in-folder-command':
  1611.       showInFolder();
  1612.       break;
  1613.     case 'open-in-new-tab-command':
  1614.     case 'open-in-background-tab-command':
  1615.       openBookmarks(LinkKind.BACKGROUND_TAB);
  1616.       break;
  1617.     case 'open-in-new-window-command':
  1618.       openBookmarks(LinkKind.WINDOW);
  1619.       break;
  1620.     case 'open-incognito-window-command':
  1621.       openBookmarks(LinkKind.INCOGNITO);
  1622.       break;
  1623.     case 'delete-command':
  1624.       deleteBookmarks();
  1625.       break;
  1626.     case 'copy-command':
  1627.       chrome.bookmarkManagerPrivate.copy(getSelectedBookmarkIds(),
  1628.                                          updatePasteCommand);
  1629.       break;
  1630.     case 'cut-command':
  1631.       chrome.bookmarkManagerPrivate.cut(getSelectedBookmarkIds(),
  1632.                                         updatePasteCommand);
  1633.       break;
  1634.     case 'paste-command':
  1635.       selectItemsAfterUserAction(list);
  1636.       chrome.bookmarkManagerPrivate.paste(list.parentId,
  1637.                                           getSelectedBookmarkIds());
  1638.       break;
  1639.     case 'sort-command':
  1640.       chrome.bookmarkManagerPrivate.sortChildren(list.parentId);
  1641.       break;
  1642.     case 'rename-folder-command':
  1643.     case 'edit-command':
  1644.       if (document.activeElement == tree)
  1645.         tree.selectedItem.editing = true;
  1646.       else {
  1647.         var li = list.getListItem(list.selectedItem);
  1648.         if (li)
  1649.           li.editing = true;
  1650.       }
  1651.       break;
  1652.     case 'new-folder-command':
  1653.       newFolder();
  1654.       break;
  1655.     case 'add-new-bookmark-command':
  1656.       addPage();
  1657.       break;
  1658.     case 'open-in-same-window-command':
  1659.       openItem();
  1660.       break;
  1661.     case 'undo-delete-command':
  1662.       undoDelete();
  1663.       break;
  1664.   }
  1665. }
  1666.  
  1667. // Delete on all platforms. On Mac we also allow Meta+Backspace.
  1668. $('delete-command').shortcut = 'U+007F' +
  1669.                                (cr.isMac ? ' U+0008 Meta-U+0008' : '');
  1670.  
  1671. // Global undo is Ctrl-Z (Command-Z on Mac). It is not in any menu.
  1672. $('undo-command').shortcut = (cr.isMac ? 'Meta' : 'Ctrl') + '-U+005A';
  1673.  
  1674. $('open-in-same-window-command').shortcut = cr.isMac ? 'Meta-Down' :
  1675.                                                        'Enter';
  1676.  
  1677. $('open-in-new-window-command').shortcut = 'Shift-Enter';
  1678. $('open-in-background-tab-command').shortcut = cr.isMac ? 'Meta-Enter' :
  1679.                                                           'Ctrl-Enter';
  1680. $('open-in-new-tab-command').shortcut = cr.isMac ? 'Shift-Meta-Enter' :
  1681.                                                    'Shift-Ctrl-Enter';
  1682.  
  1683. $('rename-folder-command').shortcut = $('edit-command').shortcut =
  1684.     cr.isMac ? 'Enter' : 'F2';
  1685.  
  1686. document.addEventListener('command', handleCommand);
  1687.  
  1688. // Execute the copy, cut and paste commands when those events are dispatched by
  1689. // the browser. This allows us to rely on the browser to handle the keyboard
  1690. // shortcuts for these commands.
  1691. (function() {
  1692.   function handle(id) {
  1693.     return function(e) {
  1694.       var command = $(id);
  1695.       if (!command.disabled) {
  1696.         command.execute();
  1697.         if (e) e.preventDefault();  // Prevent the system beep.
  1698.       }
  1699.     };
  1700.   }
  1701.  
  1702.   // Listen to copy, cut and paste events and execute the associated commands.
  1703.   document.addEventListener('copy', handle('copy-command'));
  1704.   document.addEventListener('cut', handle('cut-command'));
  1705.  
  1706.   var pasteHandler = handle('paste-command');
  1707.   document.addEventListener('paste', function(e) {
  1708.     // Paste is a bit special since we need to do an async call to see if we can
  1709.     // paste because the paste command might not be up to date.
  1710.     updatePasteCommand(pasteHandler);
  1711.   });
  1712. })();
  1713.